feat(spec): declare IMetadataService.loadManyKeyed beside its plural-read siblings - #19609
Conversation
…read siblings The keyed plural loader read shipped as a public member on MetadataManager with no declaration on the contract its two siblings loadMany? and loadDiagnosed? are declared on, so the one cross-package caller — the ObjectQL governance audit — narrowed the service slot with a local structural type written beside the call site. Declare the member on IMetadataService, delete the local type, and let the call site read the contract. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
A double holding one fixed body cannot satisfy a caller-chosen `T` — the three object-literal implementations were concrete and `check:test-typecheck` refused them (TS2322, a signature the shrink-only ledger does not record). Declare each double `<T = unknown>` and hand the body back under it. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 216cc3273f3c24c061ee4b87084f52532fdeb14a && git checkout 216cc3273f3c24c061ee4b87084f52532fdeb14a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 b96baa08f273ba99c0986c24f706b693c2061b6a && git checkout -B drift-repro 5c5b67fc4140f76ca3158acea9e0845d9eebfad8 && git merge --no-ff b96baa08f273ba99c0986c24f706b693c2061b6a
node scripts/docs-audit/affected-docs.mjs --json 5c5b67fc4140f76ca3158acea9e0845d9eebfad8
|
…probe-first example Three corrections to the loadManyKeyed declaration and its page. The docblock cited the card that ordered the implementation, which has since been deleted from the board and no longer resolves; check:issue-citations refused it as allocated-but-absent. Cite the merged pull request that is the live record instead, and keep the deleted card's number in prose, without a leading hash, so it stays greppable without minting a reference that dangles. "a customization container" named nothing in the tree and collided with the ADR-0005 sys_metadata overlay, which is a different mechanism this very page documents. The real case is the aggregated defineView container, which the MetadataKeyedItem docblock already names; both carriers now say so. The page's example did an optional call and null-coalesced to an empty array, turning an absent member into an empty set — the opposite of what the callout beneath it says and of what the one real caller does. It now probes first and spells out why absence is not emptiness. Claude-Session: https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx Co-authored-by: Claude <noreply@anthropic.com>
Ready, green, reviewed — and the last step is blocked with no channelAll three landing preconditions are met and recorded on the card:
This PR has been flipped draft → ready, confirmed by ⛔ ⛔ The seat will not merge this PR, enqueue it by hand, or submit an approving review on it. The only remaining act is enabling auto-merge with Action needed from the maintainer or a seat with the channel: enable auto-merge (SQUASH). Everything else here is finished. Generated by Claude Code |
✅ Correction — this PR is NOT blocked any more. It is in the merge queue.
The maintainer re-granted the permission and the call was retried. ⛔ The earlier standing-down note on this PR — "
|
Carrier stripped on a PASS that is on record — and the seat's own enqueue error, stated plainly
⛔ What I got wrongI enqueued this PR at 00:35Z while
The guard was right and the rule it cites is one I had already read: 「双肢命中任一 ⇒ 无席内条款②复核 PASS 在案 ⛔ 禁止入队」. I ran the three landing preconditions (at-tier PASS,
✅ Why stripping now is the sanctioned act and ⛔ not a way past the checkThe guard's own text draws the line: "⛔ Stripping the label to get past this check, with no verdict on record, is the defect this leg was built from — not a way through it." There is a verdict on record, and it is cited here rather than asserted: At-tier contract review, ⇒ the condition 「PASS ⇒ 同席剥标并引记录」 is satisfied. The carrier is stripped from both carriers — this PR and its card — by the same seat that adopted the verdict, in the four-step label write, with read-back. What happens nextCarrier stripped on both sides → Generated by Claude Code |
Fixes #15385
Declares
loadManyKeyed?onIMetadataService, deletes the local structural type the ObjectQL governance audit used to reach it, and documents the member.Execution of the recorded ruling — director seat, decision batch #123 item 5, 2026-09-12, comment
5644711080, maintainer verbatim 「同意」. That ruling picks option 1 of the two the card put to triage and enumerates four items; all four are below. Option 2 (leave it undeclared on purpose) is ruled out and is not re-opened here.Clause-②: yes
The declaration
packages/spec/src/contracts/metadata-service.ts— the new member sits immediately after its unkeyed twinloadMany?(line 675 on the base commit), inside the sameIMetadataServicedeclaration that already carriesloadMany?andloadDiagnosed?.loadManyKeyed?is optional, is generic in one parameterTthat defaults tounknown, takestype: stringplus an optionaloptionsbag typed as a Record from string to unknown, and resolves to an Array of{ name: string; data: T }pairs. That is the signature the ruling names, member for member. The diff is the authority on it, not this paragraph.It is optional, like both siblings, so every existing
IMetadataServiceimplementation still satisfies the contract unchanged and thetypeof ... === 'function'probe stays the way a caller asks for it.Why an inline pair shape rather than the published
MetadataKeyedItemThe ruling writes the return type inline, and that is also the only spelling available.
MetadataKeyedItemis declared inpackages/metadata/src/loaders/loader-interface.tsand exported from@objectstack/metadata, which depends on@objectstack/spec;packages/specdeclares no workspace dependency at all (pg-connection-stringandzod). Importing the named type here would invert that edge and close a cycle. The inline pair is also the file's own precedent:loadDiagnosed?declares its result inline in exactly the same way.The two shapes are structurally identical —
MetadataKeyedItemisreadonly name: stringbesidereadonly data: T, and a readonly property is assignable to a mutable one — soMetadataManager implements IMetadataServicekeeps compiling with no edit topackages/metadata, which is what the green build below shows.The four ruled items
loadDiagnosed's cites#4127 batch 4.packages/objectql/src/plugin.tslosesKeyedPluralMetadataRead(the type and its docblock), and the three service lookups inresolveGovernanceMetadataServicenow ask forIMetadataServicealone instead of intersecting it. Occurrences of that type name underpackages/go 4 to 0 — 1 declaration plus 3 use sites; an earlier draft of this line said 3, corrected against the blob by the at-tier review (lit control:IMetadataServicein the same file = 13, so the zero is a reading).check:slot-lookupstays green — see below.content/docs/kernel/contracts/metadata-service.mdxgains the member, in the interface excerpt'sLoader reads (optional)group and as a newloadManyKeyedsubsection. Section choice and a contrary fact about it are in the acceptance notes.Clause-②: yesabove;minorchangeset (@objectstack/spec, whose changesetsfixedgroup already carries@objectstack/objectql). Theneeds:contract-reviewcarrier was not hung by this branch — the dev never wrote a label.Round 2 — head
b96baa08f2(2 files, +38 / −13)Three corrections, all prose; the PR's file list is unchanged at 5 and no new path was pulled in.
check:issue-citationswas RED at97a639c5b3: the new docblock cited an issue that returns 404 (LIT CONTROL: its neighbour#14424→ 200, so the 404 is a reading). ⭐ The replacement was not guessed —#15378was verified four ways before being named: HTTP 200,merged: true,merged_at, basemain, and the depth-immune one —origin/mainholds the implementation it added (git grep 'async loadManyKeyed' origin/main -- packages/metadata/src/metadata-manager.ts= 1; nonsense control = 0).check-issue-citationsprints a remedy arm its own grammar cannot honour — 「keep the number and say in prose」 still fails, becauseCITATION_REsees the##19614: keeping the#still matchesCITATION_RE, andNON_CITATION_HEADSexcuses only ordinal heads — there is no prose-acknowledgement mechanism in the script. So the dead card is kept as bare digits without a leading hash, with a sentence saying why. Greppable, and nothing dangles.[], turning absence into an empty set — on the one member whose reason for existing is that silent drops are dangerous. It now reads the member into a local, guards ontypeof === 'function', and itselsebranch says why absence is not emptiness, matchingplugin.ts.?? []is gone from the page (0 hits).origin/mainacrosspackages/,content/anddocs/. The right vocabulary came fromMetadataKeyedItem's own docblock: an aggregateddefineViewcontainer "has no ownnameBY DESIGN (its identity is the target object)". Both carriers now say that, each with an explicit disclaimer that it is not the ADR-0005sys_metadataorg customization overlay — which this same page documents separately.Gate readings at⚠️
b96baa08f2—check:issue-citationsis recorded here and ⛔ not in the derived-families row (which is below, in the Local runs table — an earlier draft of this line said "above"), because its root script is--self-testonly while CI runs the self-test and the scan; reporting the alias as a pass is what produced the red in the first place. Run as the SCAN:node scripts/check-issue-citations.mjs --base origin/main→ EXIT=0, captured before any pipe, re-run at the final head → EXIT=0 (4 citations judged across 13 files; 2 resolves, 2 resolves-as-pull-request).pnpm lintwhole repo EXIT=0 · spec build success ·check:generatedall 15 up to date against a fresh build · spec typecheck pass · spec test 509 files / 14901 passed · objectql typecheck EXIT=0 ·check:slot-lookupholds ·check:nul-bytesOK plus a hand control-character scan of both edited files.Mechanical proof the
.tsedit is docblock-only: every added and removed line ingit diff 97a639c5b3..b96baa08f2 -- packages/spec/src/contracts/metadata-service.tsis a comment line — zero non-comment lines. No type or runtime surface moved, so the ablation recorded at97a639c5b3still stands and was not re-run.check:docs-transcript-driftexit 3 (@objectstack/lintunbuilt) → built, re-ran, EXIT=0; objectql typecheck first exit 2 with 42 errors, allTS2307 Cannot find modulefrom an unbuilt dependency closure → built, re-ran, EXIT=0.git merge-base --is-ancestoron #15378's squash commit exited 1, but this checkout is shallow and the control leg was a shallow-window near-relative — so that negative is void, not evidence. The tree read and the API'smerged/merged_atanswer the question without a history walk.Verification
Reverse verification, because this is a cross-package type change and a green typecheck against a stale
.d.tsis indistinguishable from a real one. Run from the committed state throughscripts/ablation-replace.mjs, with the on-disk and in-distevidence the tool produces:bd37483b1715to3a9e85d219ad.scripts/ablation-dist-preflight.mjsfound the mutated marker in 2 built files (packages/spec/dist/contracts/index.d.tsand.d.mts), so the run below read the rebuilt declarations and not a cache.tsc --noEmitinpackages/objectqlwent red with exactly one error, and it is the call site:src/plugin.ts(2593,35): error TS2339: Property 'loadManyKeyed' does not exist on type 'IMetadataService'.bd37483b1715, equal to HEAD,git diff HEADempty, whole-treegit status --porcelainempty. After a rebuild the mutated marker is gone fromdist(0 occurrences) and the real member is back (2), andtsc --noEmitinpackages/objectqlis green with zero output.That is the proof for ruled item 2: the call site now reads the contract, and it reads only the contract.
Local runs.⚠️ Provenance corrected — this table is not all from one head. The nine readings restated in the Round 2 section were taken at the final head
b96baa08f2; every other row here — the objectql typecheck, the changeset gates, the 14 docs gates and the 20 further derived families — was measured at97a639c5b3, before round 2 rewrote the.mdx. ⛔ Nothing is actually unmeasured at the final head: CI ran the whole docs family green there, including "packages/spec/src/**doc-block symbol anchors resolve". It is the sentence that over-claimed its own provenance, not the work.pnpm lint(whole repo,eslint . --no-inline-config)pnpm --filter @objectstack/spec buildpnpm --filter @objectstack/spec check:generatedpnpm --filter @objectstack/spec typecheckcheck:test-typecheck)pnpm --filter @objectstack/spec testpnpm --filter @objectstack/objectql typecheckpnpm --filter @objectstack/objectql testpnpm check:slot-lookup0e658fb: no files addedpnpm check:nul-bytescheck:empty-changeset,check:adr-0087-registration,check:changeset-no-major,check:changeset-gate-self-tests)check:doc-anchors,check:doc-authoring,check:doc-frontmatter,check:docs-section-name,check:docs-single-h1,check:docs-redirects,check:docs-spec-enumerations,check:docs-transcript-drift,check:docs-audit-scope,check:doc-route-spelling,docs-audit/check-affected-docs,docs-audit/check-drift-comment,check:section-landing-index,check:keyed-text-bounds)check:type-check-coverage,check:test-source-alias,check:published-files,check:dts-closure,check:lean-entry-closure,check:cross-package-test-inputs,check:spec-docblock-symbol-anchors,check:comment-mask-adoption,check:comment-mask-corpus,check:undeclared-dep-imports,check:query-options-erasure,check:spec-parsed-alias,check:objectql-double-limit,check:engine-double-contract,check:durability-log-level,check:published-readme-links,check:pm-prior-rulings,check:sourcemap-no-sources-content,check:strictness-ledger,check:skill-refs)check:type-check-debt,check:dual-build-cjs-loadsPREREQUISITE NOT MET); each needs a whole-workspace build this branch did not run. Neither a pass nor a finding. Declared to CI.What the generators actually moved: nothing
Measured rather than inferred, and this was the one prediction worth testing. Six generators were run against the built tree —
gen:api-surface,gen:export-origins,gen:spec-changes,gen:schema,gen:docs,gen:declaration-map— each exiting 0, after whichgit status --porcelainlisted no generated artefact.check:generatedindependently reports all 15 up to date. So an optional member on a published interface moves none of the four artefacts that nameIMetadataService, exactly as the claim predicted.check:api-surfacefirst reported stale withPREREQUISITE NOT MET — this gate reads built output, and what is on disk predates the sources. Thedisthad been built before a later edit to the test file, which is a build input. Rebuilding cleared it. It was never an artefact move.Acceptance notes
⛔ Noted, not filed, and deliberately not fixed here — each is outside this card's ruled four items.
loadManyKeyedahead of its own declared siblingloadMany?. Oncontent/docs/kernel/contracts/metadata-service.mdx,loadManyappeared 0 times before this change (lit control on the same page:loadDiagnosed= 5, so the zero is a reading). The page's interface excerpt is explicitly partial and says so — its line 29 points atIMetadataServicein the source for the full member list — so this is a documentation gap rather than a contradiction, but the ordering is odd for a reader and it is being handed to the seat to file as its own card. Widening this PR to also documentloadManywas declined on purpose.loadManyKeyedsubsection underCore CRUD, immediately afterload / loadDiagnosedand beforelist / listNames.Bulk Operationswas considered and rejected: despite the name, that section on this page documents bulk writes (bulkRegister/bulkUnregister), so a plural loader read filed there would sit in the write section. The chosen spot is the page's loader-read run, one step from the plural registry reads a reader would be comparing it against.loadDiagnosedexample still teaches the shape this PR's new example refuses. Atcontent/docs/kernel/contracts/metadata-service.mdx:138, two sections above the new probe-first example, the pre-existingloadDiagnosedsnippet spells an optional call plus?? {}— absence collapsing into a value, which is exactly what the new example'selsebranch says not to do ("Do NOT fall through to an empty set") and what the info Callout restates ("never as an empty set"). It is present at this PR's merge base and untouched here (?? []on this page at head: 0, git grep exit 1 captured before any pipe; lit control?? {}on the same page: 1, at:138, so the zero is a reading). The page is now internally inconsistent in style rather than wrong. ⛔ Recorded here rather than filed as a card, per the standing rule: the question "which PR will touch this file?" has an answer, and it is this one — so the note belongs where the next editor of the page will read it. Widening this PR to rewrite a snippet outside its four ruled items was declined on purpose.#16090serialisation caveat recorded in the ruling's item 3 is spent. That issue is closed, and no open pull request holds the page. Nothing was serialised against and nothing waited.Landing
⛔ Draft on purpose, and it stays that way from this branch. No flip to ready, no enqueue, no auto-merge. Landing is the owning seat's act after an at-tier contract review.
Generated by Claude Code